package compiler;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Hashtable;

import ast.ArgList;
import ast.Attribution;
import ast.Expression;
import ast.ExpressionList;
import ast.Function;
import ast.FunctionCall;
import ast.FunctionCallStatement;
import ast.IfStatement;
import ast.IntegerExpr;
import ast.Operation;
import ast.Operator;
import ast.PrintStatement;
import ast.Program;
import ast.ReadStatement;
import ast.ReturnStatement;
import ast.Statement;
import ast.StatementList;
import ast.StringExpr;
import ast.Type;
import ast.VarDec;
import ast.VarDecList;
import ast.Variable;
import ast.WhileStatement;

public class Compiler {

	private ScopeManager symbolTable;
	private Lexer lexer;
	private Hashtable<Symbol, Type> typeMap = new Hashtable<Symbol, Type>();
	private Hashtable<Symbol, Operator> operatorMap = new Hashtable<Symbol, Operator>();
	private OutputFormatter of;
	private Type currentFunctionType;
	private boolean errorDetected = false;

	/**
	 * Default constructor
	 */
	public Compiler() {
		// Map symbols to their AST objects
		typeMap.put(Symbol.INT, Type.INT);
		typeMap.put(Symbol.STR, Type.STR);

		operatorMap.put(Symbol.PLUS, Operator.ADD);
		operatorMap.put(Symbol.MINUS, Operator.SUB);
		operatorMap.put(Symbol.MULT, Operator.MUL);
		operatorMap.put(Symbol.DIV, Operator.DIV);
		operatorMap.put(Symbol.LT, Operator.LT);
		operatorMap.put(Symbol.GT, Operator.GT);
		operatorMap.put(Symbol.LE, Operator.LE);
		operatorMap.put(Symbol.GE, Operator.GE);
		operatorMap.put(Symbol.EQ, Operator.EQ);
		operatorMap.put(Symbol.NE, Operator.NE);
		operatorMap.put(Symbol.AND, Operator.AND);
		operatorMap.put(Symbol.OR, Operator.OR);
	}

	/**
	 * Parses the code given by the input file, creates the AST and then
	 * generates the C source code
	 * 
	 * @param ifname
	 *            The path to the input file containing the code
	 * @param ofname
	 *            The path to the output file containing the generated C code
	 * @throws IOException
	 *             If the input file doesn't exists and/or doesn't have read
	 *             permission
	 */
	public void compile(String ifname, String ofname) throws IOException {
		File f = new File(ifname);
		FileInputStream finp = new FileInputStream(f);
		BufferedReader in = new BufferedReader(new InputStreamReader(finp));
		char[] input = new char[(int) f.length()];
		in.read(input, 0, (int) f.length());

		symbolTable = new ScopeManager();
		lexer = new Lexer(input);

		lexer.nextToken();
		Program program = program();
		if(!errorDetected) {
			of = new OutputFormatter(ofname);
			program.genC(of);
			of.close();
		}
	}

	/**
	 * Parses the program nonterminal and creates the AST node of this part
	 * 
	 * @return Program node of the AST
	 */
	private Program program() {
		ArrayList<Function> functions = new ArrayList<Function>();
		while(lexer.getToken() != Symbol.EOF) {
			functions.add(function());
		}
		if(symbolTable.getGlobal("main") == null)
			error("main function not declared");
		return new Program(functions);
	}
	
	/**
	 * Parses the function nonterminal and creates the AST node of this part
	 * @return Function node of the AST
	 */
	private Function function() {
		lexer.nextToken();
		symbolTable.clearLocals();

		if (lexer.getToken() != Symbol.IDENT)
			Utils.error("Identifier expected", lexer.getCurrentContext());

		String fName = lexer.getStringValue();
		lexer.nextToken();

		if (lexer.getToken() != Symbol.COLON)
			Utils.error("Colon expected", lexer.getCurrentContext());

		lexer.nextToken();

		ArgList args = argList();
		Type t = null;
		if (lexer.getToken() == Symbol.ARROW) {
			lexer.nextToken();
			if (lexer.getToken() != Symbol.INT
					&& lexer.getToken() != Symbol.STR)
				Utils.error("type expected", lexer.getCurrentContext());
			t = typeMap.get(lexer.getToken());
			lexer.nextToken();
		}
		currentFunctionType = t;
		Function func = new Function(t, fName, args);
		
		if(fName.equals("main")) {
			if(args.getSize() != 0)
				error("main function must not have any arguments");
			if(t != null)
				error("main function must not return a value");
		}
		
		symbolTable.insertGlobal(fName, func);

		verifyTerminal(Symbol.COMMA);

		func.setVdl(varDecList());
		func.setStl(statementList());
		
		if(currentFunctionType != null && !func.getStl().hasReturnStatement(currentFunctionType)) {
			String typeName = "";
			if (currentFunctionType == Type.INT)
				typeName = "integer";
			else if (currentFunctionType == Type.STR)
				typeName = "string";
			else if (currentFunctionType == Type.BOOL)
				typeName = "boolean value";
			Utils.error("function " + func.getName() + " must return " + typeName , lexer.getCurrentContext());			
		}
		
		verifyTerminal(Symbol.DOT);
		
		return func;

	}
	
	/**
	 * Parses the argList nonterminal and creates the AST node of this part
	 * @return ArgList node of the AST
	 */
	private ArgList argList() {
		ArrayList<Variable> args = new ArrayList<Variable>();

		if (lexer.getToken() == Symbol.INT || lexer.getToken() == Symbol.STR) {
			args.add(arg());
			while (lexer.getToken() == Symbol.SEMICOLON) {
				lexer.nextToken();
				args.add(arg());
			}
		}

		return new ArgList(args);

	}

	/**
	 * Parses the arg nonterminal and creates the AST node of this part
	 * @return Variable node of the AST
	 */
	private Variable arg() {
		Type t = typeMap.get(lexer.getToken());
		lexer.nextToken();
		if (lexer.getToken() != Symbol.IDENT)
			Utils.error("Identifier expected", lexer.getCurrentContext());
		String name = lexer.getStringValue();
		lexer.nextToken();
		Variable argument = new Variable(t, name);
		if (symbolTable.insertLocal(name, argument) != null)
			Utils.error("Duplicate variable", lexer.getCurrentContext());
		return argument;
	}

	/**
	 * Parses the returnStatement nonterminal and creates the AST node of this part
	 * @return ReturnStatement node of the AST
	 */		
	private ReturnStatement returnStatement() {
		if (currentFunctionType == null)
			Utils.error("return statement in a procedure", lexer
					.getCurrentContext());
		lexer.nextToken();
		Expression e = expr();
		if(currentFunctionType != null)
			checkExpr(e, currentFunctionType);
		return new ReturnStatement(e);
	}

	/**
	 * Parses the statement_list nonterminal and creates the AST node of this
	 * part
	 * 
	 * @return StatementList node of the AST
	 */
	private StatementList statementList() {
		ArrayList<Statement> list = new ArrayList<Statement>();

		Symbol token = lexer.getToken();

		while (token == Symbol.INT || token == Symbol.STR
				|| token == Symbol.IDENT || token == Symbol.PRINT
				|| token == Symbol.READ || token == Symbol.IF
				|| token == Symbol.WHILE || token == Symbol.RETURN) {
			if (token == Symbol.IDENT) {
				Object o = symbolTable.get(lexer.getStringValue());
				if(o instanceof Function)
					list.add(functionCallStatement());
				else if(o instanceof Variable)
					list.add(varAttr());
				else {
					error("variable or function '" + lexer.getStringValue() + "' undeclared");
					lexer.skipToNextStatement(false);
				}
					
			} else if (token == Symbol.PRINT) {
				list.add(print());
			} else if (token == Symbol.READ) {
				list.add(read());
			} else if (token == Symbol.IF) {
				list.add(ifStatement());
			} else if (token == Symbol.WHILE) {
				list.add(whileStatement());
			} else if (token == Symbol.INT || token == Symbol.STR) {
				error("variables must be declared at the beginning of the scope");
			} else if (token == Symbol.RETURN) {
				list.add(returnStatement());
			}

			if (token != Symbol.IF && token != Symbol.WHILE)
				verifyTerminal(Symbol.SEMICOLON);

			token = lexer.getToken();
		}

		return new StatementList(list);
	}

	/**
	 * Parses the vardec nonterminal and creates the AST node of this part
	 * 
	 * @return VarDec node of the AST
	 */
	private VarDec varDec() {
		Type t = typeMap.get(lexer.getToken());
		String n = null;

		lexer.nextToken();
		if (lexer.getToken() == Symbol.IDENT)
			n = lexer.getStringValue();

		Variable v = new Variable(t, n);
		if (symbolTable.insertLocal(v.getName(), v) != null)
			error("variable '" + v.getName() + "' was declared previously");
		lexer.nextToken();
		return new VarDec(v);
	}

	/**
	 * Parses the vardec_list nonterminal and creates the AST node of this part
	 * 
	 * @return VarDecList node of the AST
	 */
	private VarDecList varDecList() {
		ArrayList<VarDec> list = new ArrayList<VarDec>();

		while (lexer.getToken() == Symbol.INT || lexer.getToken() == Symbol.STR) {
			list.add(varDec());
			verifyTerminal(Symbol.SEMICOLON);
		}

		return new VarDecList(list);
	}

	/**
	 * Parses the expr nonterminal and creates the AST node of this part
	 * 
	 * @return Expression node of the AST
	 */
	private Expression expr() {
		Expression expr = null;
		if (lexer.getToken() == Symbol.MINUS) {
			lexer.nextToken();
			if (lexer.getToken() != Symbol.NUMBER)
				error("number expected after '-' sign");
			expr = new IntegerExpr(-lexer.getNumericValue());
			lexer.nextToken();
		} else if (lexer.getToken() == Symbol.NUMBER) {
			expr = new IntegerExpr(lexer.getNumericValue());
			lexer.nextToken();
		} else if (lexer.getToken() == Symbol.STRINGVALUE) {
			expr = new StringExpr(lexer.getStringValue());
			lexer.nextToken();
		} else if (lexer.getToken() == Symbol.LEFTPAR) {
			expr = operation();
		} else if (lexer.getToken() == Symbol.IDENT) {
			Object o = symbolTable.get(lexer.getStringValue());
			if (o == null) {
				error("variable or function '" + lexer.getStringValue() + "' is not declared");
				lexer.nextToken();
			}
			else if(o instanceof Function)
				expr = functionCall();
			else {
				expr = (Expression) o;
				lexer.nextToken();
			}
		} else {
			error("Invalid expression");
		}
		return expr;
	}
	
	/**
	 * Parses the FunctionCall nonterminal when it's used as a statement and creates the AST node of this part
	 * @return FunctionCallStatement node of the AST
	 */
	private FunctionCallStatement functionCallStatement() {
		return new FunctionCallStatement(functionCall());
	}

	/**
	 * Parses the FunctionCall nonterminal and creates the AST node of this part
	 * @return FunctionCallStatement node of the AST
	 */	
	private FunctionCall functionCall() {
		if (lexer.getToken() != Symbol.IDENT)
			error("identifier expected");
		String fName = lexer.getStringValue();
		Function func = (Function) symbolTable.getGlobal(fName);
		if (func == null)
			error("function not found");
		lexer.nextToken();

		ArrayList<Expression> exprList = new ArrayList<Expression>();
		if(!verifyTerminal(Symbol.LEFTPAR, false)) {
			int argPos = 0;
			if (lexer.getToken() == Symbol.MINUS
					|| lexer.getToken() == Symbol.NUMBER
					|| lexer.getToken() == Symbol.STRINGVALUE
					|| lexer.getToken() == Symbol.LEFTPAR
					|| lexer.getToken() == Symbol.IDENT) {
				Expression e = expr();
				Type expectedType = func.getArgType(argPos);
				argPos++;
				if(expectedType != null)
					checkExpr(e, expectedType);
				exprList.add(e);
				
				while (lexer.getToken() == Symbol.COMMA) {
					lexer.nextToken();
					e = expr();
					expectedType = func.getArgType(argPos);
					argPos++;
					if(expectedType != null)
						checkExpr(e, expectedType);
					exprList.add(e);
				}
			}
			
			if(exprList.size() != func.getNumberOfArguments())
				error("incompatible number of arguments to function " + func.getName());
			verifyTerminal(Symbol.RIGHTPAR, false);
		}

		return new FunctionCall(func, new ExpressionList(exprList));
	}

	/**
	 * Parses the varattr nonterminal and creates the AST node of this part
	 * 
	 * @return Attribution node of the AST
	 */
	private Attribution varAttr() {
		String name = lexer.getStringValue();
		if (symbolTable.get(name) == null) {
			error("variable '" + name + "' undeclared");
			lexer.skipToNextStatement(false);
			return null;
		} 
		lexer.nextToken();
		verifyTerminal(Symbol.ASSIGN);
		
		Object o = symbolTable.get(name);
		Variable v = null;
		if(!(o instanceof Variable))
			error("variable expected");
		else
			v = (Variable) o;
		
		
		Expression e = expr();
		
		checkExpr(e, v.getType());
		
		return new Attribution(v, e);
	}

	/**
	 * Parses the if nonterminal and creates the AST node of this part
	 * 
	 * @return IfStatement node of the AST
	 */
	private IfStatement ifStatement() {
		lexer.nextToken();

		Operation operation = operation();
		checkExpr(operation, Type.BOOL);

		StatementList ifBlock = block();

		if (lexer.getToken() == Symbol.ELSE) {
			lexer.nextToken();
			StatementList elseBlock = block();
			return new IfStatement(operation, ifBlock, elseBlock);
		} else {
			return new IfStatement(operation, ifBlock);
		}
	}

	/**
	 * Parses the operation nonterminal and creates the AST node of this part
	 * @return Operation node of the AST
	 */
	private Operation operation() {
		lexer.nextToken();

		Expression left;
		if (lexer.getToken() == Symbol.LEFTPAR)
			left = operation();
		else
			left = expr();

		Operator operator = operatorMap.get(lexer.getToken());
		if (operator == null)
			error("'" + lexer.getToken().toString() + "' is not a valid operator");
		lexer.nextToken();

		Expression right;
		if (lexer.getToken() == Symbol.LEFTPAR)
			right = operation();
		else
			right = expr();

		verifyTerminal(Symbol.RIGHTPAR);

		return new Operation(left, operator, right);
	}

	/**
	 * Parses the while nonterminal and creates the AST node of this part
	 * 
	 * @return WhileStatement node of the AST
	 */
	private WhileStatement whileStatement() {
		lexer.nextToken();

		Operation operation = (Operation) operation();
		checkExpr(operation, Type.BOOL);

		StatementList whileBlock = block();

		return new WhileStatement(operation, whileBlock);
	}

	/**
	 * Parses the block nonterminal and creates the AST node of this part
	 * 
	 * @return StatementList node of the AST
	 */
	private StatementList block() {
		verifyTerminal(Symbol.COMMA);

		StatementList s = statementList();

		verifyTerminal(Symbol.DOT);
		return s;
	}

	/**
	 * Parses the print nonterminal and creates the AST node of this part
	 * 
	 * @return PrintStatement node of the AST
	 */
	private PrintStatement print() {
		lexer.nextToken();
		String name = lexer.getStringValue();
		lexer.nextToken();
		Variable v = (Variable) symbolTable.getLocal(name);
		if (v == null)
			error("variable '" + name + "' is not declared");
		return new PrintStatement(v);
	}

	/**
	 * Parses the read nonterminal and creates the AST node of this part
	 * 
	 * @return ReadStatement node of the AST
	 */
	private ReadStatement read() {
		lexer.nextToken();
		String name = lexer.getStringValue();
		lexer.nextToken();
		Variable v = (Variable) symbolTable.getLocal(name);
		if (v == null)
			error("variable '" + name + "' is not declared");
		return new ReadStatement(v);
	}

	/**
	 * Verifies if a given Symbol and the current token match
	 * @param s the Symbol to be tested
	 * @param skipSemicolon if true, go to the next statement; otherwise, stop at the semicolon (at the end of the current statement)
	 * @return true if the given symbol and the token match. False otherwise
	 */
	private boolean verifyTerminal(Symbol s, boolean skipSemicolon) {
		if (lexer.getToken() != s) {
			error("'" + s + "' expected, got '" + lexer.getToken() + "' instead");
			lexer.skipToNextStatement(skipSemicolon);
			return true;
		}
		else
			lexer.nextToken();
		return false;
	}
	
	/**
	 * Verifies if a given Symbol and the current token match
	 * @param s the Symbol to be tested
	 * @return true if the given symbol and the token match. False otherwise
	 */
	private boolean verifyTerminal(Symbol s) {
		return verifyTerminal(s, true);
	}
	
	/**
	 * Checks if a given expression and type match. If they don't, an error is showed
	 * @param expr The expression to be tested.
	 * @param type The type to be tested.
	 */
	private void checkExpr(Expression expr, Type type) {
		String typeName = "";
		if (type == Type.INT)
			typeName = "integer";
		else if (type == Type.STR)
			typeName = "string";
		else if (type == Type.BOOL)
			typeName = "boolean value";
		
		if (Utils.checkExpr(expr, type))
			error("expression must produce " + typeName);		
	}
	
	/**
	 * Shows an error
	 * @param msg The error message
	 */
	private void error(String msg) {
		Utils.error(msg, lexer.getCurrentContext());
		errorDetected = true;
	}
}
